/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.asyncweb.server.session;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.asyncweb.server.HttpSession;
import org.apache.asyncweb.server.util.LinkedPermitIssuer;
import org.apache.asyncweb.server.util.PermitExpirationListener;
import org.apache.asyncweb.server.util.TimedPermit;
import org.apache.asyncweb.server.util.TimedPermitIssuer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A simple <code>SessionStore</code> implementation which holds all session
 * data in memory. A <code>TimedPermitIssuer</code> is employed to issue a
 * time-out permit for each issued session.
 * 
 */
public class BasicSessionStore implements HttpSessionStore {

	private static final Logger LOG = LoggerFactory
			.getLogger(BasicSessionStore.class);

	/**
	 * Default session timeout of 15 minutes
	 */
	private static final long DEFAULT_SESSION_TIMEOUT = 900000;

	private Map<String, HttpSession> sessionMap = new ConcurrentHashMap<String, HttpSession>();

	private List<HttpSessionListener> listeners = new CopyOnWriteArrayList<HttpSessionListener>();

	private TimedPermitIssuer permitIssuer;

	private AtomicBoolean isClosed = new AtomicBoolean(false);

	/**
	 * Constructs with the default session timeout
	 */
	public BasicSessionStore() {
		this(DEFAULT_SESSION_TIMEOUT);
	}

	/**
	 * Constructs with a specified session timeout
	 * 
	 * @param sessionTimeout
	 *            The session timeout (in ms)
	 */
	public BasicSessionStore(long sessionTimeout) {
		permitIssuer = new LinkedPermitIssuer(sessionTimeout);
		permitIssuer.addPermitExpirationListener(new TimeoutListener());
		if (LOG.isInfoEnabled())
			LOG.info("BasicSessionStore timeout: " + sessionTimeout + "ms");
	}

	/**
	 * Adds a listener to this store
	 * 
	 * @param listener
	 *            The listener to add
	 */
	public void addSessionListener(HttpSessionListener listener) {
		listeners.add(listener);
	}

	/**
	 * Sets the listeners employed by this store. Any existing listeners are
	 * removed
	 * 
	 * @param listeners
	 *            The listeners to be added
	 */
	public void setSessionListeners(Collection<HttpSessionListener> listeners) {
		this.listeners.clear();
		this.listeners.addAll(listeners);
	}

	/**
	 * Closes this store. Our permit issuer is closed, and all sessions are
	 * destroyed.
	 */
	public void close() {

		if (!isClosed.compareAndSet(false, true)) {
			if (LOG.isDebugEnabled())
				LOG.debug("Already closed");
			return;
		}

		if (LOG.isDebugEnabled())
			LOG.debug("BasicSessionStore closing");
		permitIssuer.close();
		List<HttpSession> closureList = new ArrayList<HttpSession>(
				sessionMap.values());
		for (HttpSession httpSession : closureList) {
			BasicSession session = (BasicSession) httpSession;
			if (LOG.isDebugEnabled())
				LOG.debug("Closure: Destroying session: " + session.getId());
			session.destroy();
		}
	}

	/**
	 * Creates a new session for the specified key.
	 * 
	 * @param key
	 *            The session key
	 * @return The created session, or <code>null</code> if a session is already
	 *         held for the specified key
	 */
	public HttpSession createSession(String key) {
		if (isClosed.get()) {
			throw new IllegalStateException("Store closed");
		}
		BasicSession created = null;
		if (!sessionMap.containsKey(key)) {
			created = new BasicSession(key, this);
			sessionMap.put(key, created);
			TimedPermit permit = permitIssuer.issuePermit(created);
			created.setPermit(permit);

			if (LOG.isDebugEnabled()) {
				LOG.debug("New session created with key '" + key
						+ "'. Firing notifications");
			}
			fireCreated(created);
		}
		return created;
	}

	/**
	 * Locates the session with the specified key. If the session is found, we
	 * request it to renew its access permit.
	 * 
	 * @param key
	 *            The key for which a session is required
	 * @return The located session, or <code>null</code> if no session with the
	 *         specified key was found
	 */
	public HttpSession locateSession(String key) {
		BasicSession session = (BasicSession) sessionMap.get(key);
		if (session != null) {
			if (LOG.isDebugEnabled()) {
				LOG.debug("Located session with key '" + key
						+ "'. Marking as accessed");
			}
			session.access();
		}
		return session;
	}

	/**
	 * Invoked by a session we created when it successfully processes an expiry.
	 * The session is removed from our session map, and expiry notifications are
	 * fired.
	 * 
	 * @param session
	 *            The expired session
	 */
	void sessionExpired(BasicSession session) {
		if (LOG.isDebugEnabled()) {
			LOG.debug("Session has been expired. Processing notifications for '"
					+ session.getId() + "'");
		}
		sessionMap.remove(session.getId());
		fireExpiry(session);
	}

	/**
	 * Invoked by a session we created when it successfully processes a
	 * destruction request. The session is removed from our session map, and
	 * destruction notifications are fired
	 * 
	 * @param session
	 *            The destroyed session
	 */
	void sessionDestroyed(BasicSession session) {
		if (LOG.isDebugEnabled()) {
			LOG.debug("Session has been destroyed. Processing notifications for '"
					+ session.getId() + "'");
		}
		sessionMap.remove(session.getId());
		fireDestroyed(session);
	}

	/**
	 * Invoked when the permit associated with the specified session expires. We
	 * simply request the session to expire itself. If the session is not
	 * already destroyed, it will request us to fire notifications on its
	 * behalf.
	 * 
	 * @param session
	 *            The expired session
	 */
	private void sessionPermitExpired(BasicSession session) {
		session.expire();
	}

	/**
	 * Fires creation notification to all listeners assocaited with this store
	 * 
	 * @param session
	 *            The expired session
	 */
	private void fireCreated(HttpSession session) {
		for (HttpSessionListener listener : listeners) {
			listener.sessionCreated(session);
		}
	}

	/**
	 * Fires destruction notification to all listeners assocaited with this
	 * store
	 * 
	 * @param session
	 *            The expired session
	 */
	private void fireDestroyed(HttpSession session) {
		for (HttpSessionListener listener : listeners) {
			listener.sessionDestroyed(session);
		}
	}

	/**
	 * Fires expiry notification to all listeners assocaited with this store
	 * 
	 * @param session
	 *            The expired session
	 */
	private void fireExpiry(HttpSession session) {
		for (HttpSessionListener listener : listeners) {
			listener.sessionExpired(session);
		}
	}

	/**
	 * Receives notifications of timed out permits issued by this store, and
	 * triggers expiry of the associated session
	 * 
	 */
	private class TimeoutListener implements PermitExpirationListener {

		/**
		 * Invoked when a permit issued for a session expires
		 * 
		 * @param session
		 *            The session which has expired
		 */
		public void permitExpired(Object session) {
			sessionPermitExpired((BasicSession) session);
		}

	}

}
